//package RayTracing;

import java.awt.Color;
import java.awt.Transparency;
import java.awt.color.*;
import java.awt.image.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.imageio.ImageIO;

import org.omg.CORBA.REBIND;

import java.util.Random;


//import RayTracing.Ellipsoid;
//import RayTracing.Vector;

/**
 *  Main class for ray tracing exercise.
 */
public class RayTracer {

	public int imageWidth;
	public int imageHeight;
	public LinkedList<Renderable> objects = new LinkedList<Renderable>();
	public LinkedList<Light> lights = new LinkedList<Light>();
	public LinkedList<Material> materials = new LinkedList<Material>();
	
	private Camera camera;
	private Settings settings;
	//private int MaterialCounter = 0;

	/**
	 * Runs the ray tracer. Takes scene file, output image file and image size as input.
	 */
	public static void main(String[] args){

		try {

			RayTracer tracer = new RayTracer();

            // Default values:
			tracer.imageWidth = 500;
			tracer.imageHeight = 500;

			if (args.length < 2)
				throw new RayTracerException("Not enough arguments provided. Please specify an input scene file and an output image file for rendering.");

			String sceneFileName = args[0];
			String outputFileName = args[1];

			if (args.length > 3)
			{
				tracer.imageWidth = Integer.parseInt(args[2]);
				tracer.imageHeight = Integer.parseInt(args[3]);
			}


			// Parse scene file:
			tracer.parseScene(sceneFileName);

			// Render scene:
			tracer.renderScene(outputFileName);

//		} catch (IOException e) {
//			System.out.println(e.getMessage());
		} catch (RayTracerException e) {
			System.out.println(e.getMessage());
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}

		
	}

	/**
	 * Parses the scene file and creates the scene. Change this function so it generates the required objects.
	 */
	public void parseScene(String sceneFileName) throws IOException, RayTracerException
	{
		FileReader fr = new FileReader(sceneFileName);

		BufferedReader r = new BufferedReader(fr);
		String line = null;
		int lineNum = 0;
		System.out.println("Started parsing scene file " + sceneFileName);



		while ((line = r.readLine()) != null)
		{
			line = line.trim();
			++lineNum;

			if (line.isEmpty() || (line.charAt(0) == '#'))
			{  // This line in the scene file is a comment
				continue;
			}
			else
			{
				String code = line.substring(0, 3).toLowerCase();
				// Split according to white space characters:
				String[] params = line.substring(3).trim().toLowerCase().split("\\s+");

				if (code.equals("cam"))
				{
                                        
					Vector position = new Vector(Double.parseDouble(params[0]),
												 Double.parseDouble(params[1]),
												 Double.parseDouble(params[2]));
					
					Vector lookAtPoint = new Vector(Double.parseDouble(params[3]),
							 					 	Double.parseDouble(params[4]),
							 					 	Double.parseDouble(params[5]));
					
					Vector upVector = new Vector(Double.parseDouble(params[6]),
							 					 Double.parseDouble(params[7]),
							 					 Double.parseDouble(params[8]));
					
					float screenDistance = Float.parseFloat(params[9]);
					float screenWidth = Float.parseFloat(params[10]);
				
					camera = new Camera(position,lookAtPoint,upVector,screenDistance,screenWidth);
					System.out.println(String.format("Parsed camera parameters (line %d)", lineNum));
					
				}
				else if (code.equals("set"))
				{
                    
					settings = new Settings(Float.parseFloat(params[0]),
											Float.parseFloat(params[1]),
											Float.parseFloat(params[2]),
											Integer.parseInt(params[3]),
											Integer.parseInt(params[4]));
					System.out.println(String.format("Parsed general settings (line %d)", lineNum));
				}
				else if (code.equals("mtl"))
				{

					Material material = new Material(Float.parseFloat(params[0]),
													 Float.parseFloat(params[1]),
													 Float.parseFloat(params[2]),
													 Float.parseFloat(params[3]),
													 Float.parseFloat(params[4]),
													 Float.parseFloat(params[5]),
													 Float.parseFloat(params[6]),
													 Float.parseFloat(params[7]),
													 Float.parseFloat(params[8]),
											   		 Float.parseFloat(params[9]),
											   		 Float.parseFloat(params[10]));
					materials.add(material);
					
					System.out.println(String.format("Parsed material (line %d)", lineNum));
				}
				else if (code.equals("sph"))
				{
                   
					
					Sphere sphere = new Sphere(Double.parseDouble(params[0]),
											   Double.parseDouble(params[1]),
											   Double.parseDouble(params[2]),
											   Double.parseDouble(params[3]),
											   Integer.parseInt(params[4]));
					objects.add(sphere);

					System.out.println(String.format("Parsed sphere (line %d)", lineNum));
				}
				else if (code.equals("pln"))
				{

					Plane plane = new Plane(Double.parseDouble(params[0]), 
											Double.parseDouble(params[1]), 
											Double.parseDouble(params[2]), 
											Double.parseDouble(params[3]), 
											Integer.parseInt(params[4])); 
					objects.add(plane);

					System.out.println(String.format("Parsed plane (line %d)", lineNum));
				}
				else if (code.equals("elp"))
				{
                                    
					Ellipsoid ellipsoid = new Ellipsoid(Double.parseDouble(params[0]), 
														Double.parseDouble(params[1]), 
														Double.parseDouble(params[2]), 
														Double.parseDouble(params[3]), 
														Double.parseDouble(params[4]),
														Double.parseDouble(params[5]), 
														Double.parseDouble(params[6]), 
														Double.parseDouble(params[7]), 
														Double.parseDouble(params[8]), 
														Double.parseDouble(params[9]),
														Double.parseDouble(params[10]), 
														Double.parseDouble(params[11]), 
														Integer.parseInt(params[12]));
					objects.add(ellipsoid);

					System.out.println(String.format("Parsed ellipsoid (line %d)", lineNum));
				}
				else if (code.equals("lgt"))
				{
                    
					Vector vector = new Vector(Double.parseDouble(params[0]),
											   Double.parseDouble(params[1]),
											   Double.parseDouble(params[2]));
					
					Light light = new Light(vector, 
											Float.parseFloat(params[3]), 
											Float.parseFloat(params[4]),
											Float.parseFloat(params[5]), 
											Float.parseFloat(params[6]),
											Float.parseFloat(params[7]),
											Float.parseFloat(params[8]));
					lights.add(light);

					System.out.println(String.format("Parsed light (line %d)", lineNum));
				}
				else
				{
					System.out.println(String.format("ERROR: Did not recognize object: %s (line %d)", code, lineNum));
				}
			}
		}

                // It is recommended that you check here that the scene is valid,
                // for example camera settings and all necessary materials were defined.

		System.out.println("Finished parsing scene file " + sceneFileName);

	}
	


	/**
	 * Renders the loaded scene and saves it to the specified file location.
	 */
	public void renderScene(String outputFileName)
	{
		long startTime = System.currentTimeMillis();

		// Create a byte array to hold the pixel data:
		byte[] rgbData = new byte[this.imageWidth * this.imageHeight * 3];
		
		for(int row=0;row<this.imageHeight;row++){
			for(int col=0;col<this.imageWidth;col++){
				Ray ray = constructRayThroughPixel(col, row);
				Color color = findIntersectionColor(ray,0,0,0);
				
				rgbData[(row * this.imageWidth + col) *3] = (byte)color.getRed();
				rgbData[(row * this.imageWidth + col) *3 + 1] = (byte)color.getGreen();
				rgbData[(row * this.imageWidth + col) *3 + 2] = (byte)color.getBlue();

			}
		}


		long endTime = System.currentTimeMillis();
		Long renderTime = endTime - startTime;

                // The time is measured for your own conveniece, rendering speed will not affect your score
                // unless it is exceptionally slow (more than a couple of minutes)
		System.out.println("Finished rendering scene in " + renderTime.toString() + " milliseconds.");

                // This is already implemented, and should work without adding any code.
		saveImage(this.imageWidth, rgbData, outputFileName);

		System.out.println("Saved file " + outputFileName);

	}




	//////////////////////// FUNCTIONS TO SAVE IMAGES IN PNG FORMAT //////////////////////////////////////////

	/*
	 * Saves RGB data as an image in png format to the specified location.
	 */
	public static void saveImage(int width, byte[] rgbData, String fileName)
	{
		try {

			BufferedImage image = bytes2RGB(width, rgbData);
			ImageIO.write(image, "png", new File(fileName));

		} catch (IOException e) {
			System.out.println("ERROR SAVING FILE: " + e.getMessage());
		}

	}

	/*
	 * Producing a BufferedImage that can be saved as png from a byte array of RGB values.
	 */
	public static BufferedImage bytes2RGB(int width, byte[] buffer) {
	    int height = buffer.length / width / 3;
	    ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
	    ColorModel cm = new ComponentColorModel(cs, false, false,
	            Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
	    SampleModel sm = cm.createCompatibleSampleModel(width, height);
	    DataBufferByte db = new DataBufferByte(buffer, width * height);
	    WritableRaster raster = Raster.createWritableRaster(sm, db, null);
	    BufferedImage result = new BufferedImage(cm, raster, false, null);

	    return result;
	}

	public static class RayTracerException extends Exception {
		public RayTracerException(String msg) {  super(msg); }
	}
	
	private Ray constructRayThroughPixel(int col, int row) {
		
		double cameraDistance = camera.getScreenDistance();
		double cameraWidth = camera.getScreenWidth();
		double fl = (imageWidth/cameraWidth) * cameraDistance;
		Vector look = camera.getLook();
		Vector du = camera.getDu();
		Vector dv = camera.getDv();
		Vector normLook = look.normalizedVector();
		Vector Vp = normLook.multiplyVectorByScalar(fl)
							.subtructVector(du.multiplyVectorByScalar(imageWidth)
							.addVectors(dv.multiplyVectorByScalar(imageHeight))
							.multiplyVectorByScalar(0.5));
		Vector direction =  du.multiplyVectorByScalar(imageWidth-col)
							  .addVectors(dv.multiplyVectorByScalar(row))
							  .addVectors(Vp).normalizedVector();
		
		Ray ray = new Ray(camera.getPosition(), direction);

		return ray;
			
	}
	
	
	
	// finds the closest hit and returns it
	private Hit getIntersection(Ray ray,int recursionDepth, double start){
		double mint = Double.MAX_VALUE;
		double t;
        Renderable closestScene=null;
        
        for(Renderable object: objects){
        	
        	t=object.intersect(ray);
        	if(t>start && t<mint){
        		mint=t;
        		closestScene=object;
        	}
        	
        }
        return new Hit(ray,closestScene,recursionDepth, mint);
	}
	
	private Color findIntersectionColor(Ray ray, int recursionDepth, double start, float totalTransparency){
		
		if (recursionDepth >= settings.getMaxRecursionLevel()) return new Color(0,0,0);
		
		Hit hit = getIntersection(ray, recursionDepth, start);

        int materialIndex=0;
        Vector N = new Vector(0,0,0); 
        Renderable closestScene = hit.getObject();
        Vector intersection = (ray.origin).addVectors((ray.direction).multiplyVectorByScalar(hit.getT()));
        
        if(closestScene instanceof Sphere){
        	closestScene = (Sphere)closestScene;
        	materialIndex = ((Sphere) closestScene).getMaterial();
        	N = ((Sphere) closestScene).getCenterVector().subtructVector(intersection);
        	N = N.multiplyVectorByScalar(-1);
        	N = N.normalizedVector();
        }
        else if(closestScene instanceof Ellipsoid){
        	closestScene = (Ellipsoid)closestScene;
        	materialIndex = ((Ellipsoid) closestScene).getMaterial();
        	Vector temp = intersection.multiplyMatrixByVector(((Ellipsoid) closestScene).getTransformationMatrix());
        	N.setvx((temp.getvx() - ((Ellipsoid)closestScene).getcx()));
        	N.setvy((temp.getvy() - ((Ellipsoid)closestScene).getcy()));
        	N.setvz((temp.getvz() - ((Ellipsoid)closestScene).getcz()));
        	N = N.normalizedVector();
        	//N = N.multiplyVectorByScalar(-1);
        }
        else if( closestScene instanceof Plane){
        	closestScene = (Plane)closestScene;
        	materialIndex = ((Plane) closestScene).getMaterial();
        	N = ((Plane)closestScene).getNormal().normalizedVector();
        }
        
		if(closestScene==null){ 
			return new Color(settings.getBackgroundRed() * (1-totalTransparency), 
							 settings.getBackgroundGreen() * (1-totalTransparency), 
							 settings.getBackgroundBlue() * (1-totalTransparency));
		}
        
		Vector V = ray.origin.subtructVector(intersection).normalizedVector();
		
		
		float [] RGB = new float[3];
		Material material = materials.get(materialIndex-1);
		
		float transparency = material.getTransparency();
		
		for(Light light:lights){
			
			Vector L = new Vector(light.getPositionX()-intersection.getvx(),
								  light.getPositionY()-intersection.getvy(),
								  light.getPositionZ()-intersection.getvz()).normalizedVector();
			
			double shadow = getShadow(hit, light,N);
		
			double lightNorm = L.dotProduct(N); 
			if (lightNorm>0){
				
				RGB[0] += lightNorm * material.getDiffuseRed() * light.getRedColor() * (1-transparency) * shadow;
				RGB[1] += lightNorm * material.getDiffuseGreen() * light.getGreenColor() * (1-transparency) * shadow;
				RGB[2] += lightNorm * material.getDiffuseBlue() * light.getBlueColor() * (1-transparency) * shadow;
				for(int i=0 ; i<3 ; i++){					
					if(RGB[i]<0) RGB[i]=0;
					if(RGB[i]>1) RGB[i]=1;
				}
			}
			Vector R = N.multiplyVectorByScalar(L.multiplyVectorByScalar(2).dotProduct(N)).subtructVector(L);//.normalizedVector();
			     
	        double eyeNorm = R.dotProduct(V); // changed
	        if(eyeNorm > 0){
	        	
                double specularity = Math.pow(eyeNorm, material.getPhong());  
                
	        	
	        	RGB[0] += specularity * material.getSpecularRed() * light.getRedColor() * light.getSpecularIntensity() * (1-transparency) * shadow;
				RGB[1] += specularity * material.getSpecularGreen() * light.getGreenColor() * light.getSpecularIntensity() * (1-transparency) * shadow;
				RGB[2] += specularity * material.getSpecularBlue() * light.getBlueColor() * light.getSpecularIntensity() * (1-transparency) * shadow;
				for(int i=0 ; i<3 ; i++){
					if(RGB[i]<0) RGB[i]=0;
					if(RGB[i]>1) RGB[i]=1;
				}
			}   
		}
		
		
		// if transparent - continue to the next hit.
        if(transparency>0){
        	
        	Color color = findIntersectionColor(ray, recursionDepth+1, hit.getT(), 0);
        	float red = (float)color.getRed()/255;
        	float green = (float)color.getGreen()/255;
        	float blue = (float)color.getBlue()/255;
        	
        	RGB[0] += red*(transparency);
        	RGB[1] += green*(transparency);
        	RGB[2] += blue*(transparency);
        	
        	for(int i=0 ; i<3 ; i++){
				if(RGB[i]<0) RGB[i]=0;
				if(RGB[i]>1) RGB[i]=1;
			}	
        }
    	
    	
        // add reflection
        Vector temp = N.multiplyVectorByScalar(2*(N.dotProduct(V)))
        			   .subtructVector(V).normalizedVector();
        			// .multiplyVectorByScalar(-1);
        Ray tempRay = new Ray(intersection, temp);
        Color tempColor = findIntersectionColor(tempRay, recursionDepth+1, 0.00001,0);
        
        float red = (float)tempColor.getRed()/255;
    	float green = (float)tempColor.getGreen()/255;
    	float blue = (float)tempColor.getBlue()/255;
        
        RGB[0] += red * material.getReflectionRed();// * transparency;
        RGB[1] += green * material.getReflectionGreen();// * transparency;
        RGB[2] += blue * material.getReflectionBlue();// * transparency;
        
        for(int i=0 ; i<3 ; i++){
			if(RGB[i]<0) RGB[i]=0;
			if(RGB[i]>1) RGB[i]=1;
		}
		
		return new Color(RGB[0],RGB[1],RGB[2]);
	}
	
	
	
	private double getShadow(Hit hit, Light light, Vector N){
		
		double count = 0.0;
        int shadowRays = settings.getShadowRays();
        Random rand = new Random();
  
        
		Vector lightPos = light.getPosition();
        double lightRadius = light.getLightRadius() / 2;
        Vector intersection = hit.getIntersectionPoint();
        
        Plane plane = new Plane(N.getvx(),N.getvy(),N.getvz(), N.dotProduct(lightPos),0);
        
        Vector left = plane.getVector(-lightRadius, -lightRadius, lightPos);
        Vector down = plane.getVector(lightRadius, -lightRadius, lightPos).subtructVector(left);
        Vector right = plane.getVector(-lightRadius, lightRadius, lightPos).subtructVector(left);

        for(int i=0 ; i<shadowRays ; i++){
        	for(int j=0 ; j<shadowRays ; j++){
        		
                Vector origin = left.addVectors(down.multiplyVectorByScalar((i + rand.nextDouble()) / shadowRays)).
                					 addVectors(right.multiplyVectorByScalar((j + rand.nextDouble()) / shadowRays)).
                					 addVectors(new Vector(0.00001, 0.00001, 0.00001));
                Vector direction = intersection.subtructVector(origin).normalizedVector();
                Ray ray = new Ray(origin, direction);
                double transparency = 1;
                double minT = 0;
                Hit newHit = null;
                
                while(objects.indexOf(((newHit = getIntersection(ray, 0, minT)).getObject())) != objects.indexOf(hit.getObject()) && transparency > 0){
                	if(newHit.getObject()==null) transparency=0;
                	else{
                		Renderable object = newHit.getObject();
                		int materialIndex=0;
                		if(object instanceof Sphere)	
                			materialIndex = ((Sphere) object).getMaterial();
                		else if(object instanceof Ellipsoid)	
                			materialIndex = ((Ellipsoid) object).getMaterial();
                		else if(object instanceof Plane)	
                			materialIndex = ((Plane) object).getMaterial();
                		Material material = materials.get(materialIndex-1);
                		transparency *= material.getTransparency();
                		minT = newHit.getT();
                	}
                }

                count += transparency;

        	}
        }
        double result = (1- count/(shadowRays*shadowRays))*(light.getShadowIntensity());
        return (1-result);
		
	}

}